﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Numerics;

namespace BitcoinLibrary
{
    /**
    * A Message is a data structure that can be serialized/deserialized using both the BitCoin proprietary serialization
    * format and built-in Java object serialization. Specific types of messages that are used both in the block chain,
    * and on the wire, are derived from this class.
    *
    * This class is not useful for library users. If you want to talk to the network see the {@link Peer} class.
    */
    public abstract class Message /*implements Serializable*/
    {
        private static readonly long serialVersionUID = -3561053461717079135L;

        public static readonly int MAX_SIZE = 0x02000000;

        // Useful to ensure serialize/deserialize are consistent with each other.
        private static readonly bool SELF_CHECK = false;

        // The offset is how many bytes into the provided byte array this message starts at.
        protected /*transient*/ int offset;
        // The cursor keeps track of where we are in the byte array as we parse it.
        // Note that it's relative to the start of the array NOT the start of the message.
        protected /*transient*/ int cursor;

        // The raw message bytes themselves.
        protected /*transient*/ byte[] bytes;

        protected /*transient*/ int protocolVersion;

        // This will be saved by subclasses that implement Serializable.
        protected NetworkParameters param;

        /** This exists for the Java serialization framework to use only. */
        protected Message()
        {
        }

        public Message(NetworkParameters param)
        {
            this.param = param;
        }

        public Message(NetworkParameters param, byte[] msg, int offset, int protocolVersion)
        {
            this.protocolVersion = protocolVersion;
            this.param = param;
            this.bytes = msg;
            this.cursor = offset;
            this.offset = offset;
            parse();
            if (SELF_CHECK && !(this.GetType().Name == "VersionMessage"))
            {
                byte[] msgbytes = new byte[cursor - offset];
                Array.Copy(msg, offset, msgbytes, 0, cursor - offset);
                byte[] reserialized = bitcoinSerialize();
                if (!Array.Equals(reserialized, msgbytes))
                {
                    throw new Exception("Serialization is wrong: \n" +
                        BitConverter.ToString(reserialized) + " vs \n" +
                        BitConverter.ToString(msgbytes));
                }
            }
            this.bytes = null;
        }

        public Message(NetworkParameters param, byte[] msg, int offset)
            : this(param, msg, offset, NetworkParameters.PROTOCOL_VERSION)
        {
        }

        // These methods handle the serialization/deserialization using the custom BitCoin protocol.
        // It's somewhat painful to work with in Java, so some of these objects support a second 
        // serialization mechanism - the standard Java serialization system. This is used when things 
        // are serialized to the wallet.
        public abstract void parse();

        public byte[] bitcoinSerialize()
        {
            MemoryStream stream = new MemoryStream();
            try
            {
                bitcoinSerializeToStream(stream);
            }
            catch (IOException e)
            {
                // Cannot happen, we are serializing to a memory stream.
                throw e;
            }
            return stream.ToArray();
        }

        /**
        * Serializes this message to the provided stream. If you just want the raw bytes use bitcoinSerialize().
        */
        public virtual void bitcoinSerializeToStream(Stream stream)
        {
            throw new Exception("NYI");
        }

        public int getMessageSize()
        {
            return cursor - offset;
        }

        public long readUint32()
        {
            long u = BitConverter.ToUInt32(bytes, cursor);// Utils.readUint32(bytes, cursor);
            cursor += 4;
            return u;
        }

        public byte[] readHash()
        {
            byte[] hash = new byte[32];
            Array.Copy(bytes, cursor, hash, 0, 32);
            // We have to flip it around, as it's been read off the wire in little endian.
            // Not the most efficient way to do this but the clearest.
            hash = hash.Reverse().ToArray();// Utils.reverseBytes(hash); 
            cursor += 32;
            return hash;
        }


        public BigInteger readUint64()
        {
            // Java does not have an unsigned 64 bit type. So scrape it off the wire then flip.
            byte[] valbytes = new byte[8];
            Array.Copy(bytes, cursor, valbytes, 0, 8);
            valbytes = valbytes.Reverse().ToArray();// Utils.reverseBytes(valbytes);
            cursor += valbytes.Length;
            return new BigInteger(valbytes);
        }

        public long readVarInt()
        {
            VarInt varint = new VarInt(bytes, cursor);
            cursor += varint.getSizeInBytes();
            return varint.value;
        }


        public byte[] readBytes(int length)
        {
            byte[] b = new byte[length];
            Array.Copy(bytes, cursor, b, 0, length);
            cursor += length;
            return b;
        }

        public string readStr()
        {
            VarInt varInt = new VarInt(bytes, cursor);
            if (varInt.value == 0)
            {
                cursor += 1;
                return "";
            }
            byte[] characters = new byte[(int)varInt.value];
            Array.Copy(bytes, cursor, characters, 0, characters.Length);
            cursor += varInt.getSizeInBytes();
            try
            {
                return System.Text.Encoding.UTF8.GetString(characters);// TODO: ok?, "UTF-8");
            }
            catch (Exception e)
            {
                throw e; // Cannot happen, UTF-8 is always supported.
            }
        }
    }
}
